/*
    elasticlient API的二次封装：封装的主要思想是对各种请求的正文进行构建，请求正文是json格式的字符串
        而json格式的字符串是由Value中间类型序列化得到，所以封装就是针对Value进行构建
    
    封装的四个操作：
    1、索引创建
        提供接口，用户可以自定义索引名、文档类型（构造）
        提供接口，用户可以自定义字段类型、字段分词器、是否构建映射等属性
        提供接口，发起构建请求
    2、新增数据
        提供接口，用户可以设置数据（文档）
        提供接口，发起新增请求
    3、数据查询
        提供接口，用户可以设置应该匹配的条件
        提供接口，用户可以设置一定不匹配的条件
        提供接口，用户可以设置一定匹配的条件
        提供接口，发起检索请求
    4、数据删除
        提供接口，发起删除数据请求（删除数据只需要文档id）

    说明：根据ES的操作分别实现了ESIndex、ESInsert、ESRemove、ESSearch四个类
        没有将所有操作实现在一个类中，原因是因为不同的微服务需要的操作不同，只需创建对应操作的类，然后调用接口即可
*/
#pragma once
#include <iostream>
#include <json/json.h>
#include <elasticlient/client.h>
#include <cpr/cpr.h>
#include <memory>
#include <string>
#include "../../spdlog/spdlogger.hpp"

// 序列化
bool serialize(const Json::Value& value, std::string& json_str)
{
    // Json::StreamWriter 序列化类：用于将value数据序列化为json格式字符串的类
    // Json::StreamWriterBuilder 工厂类：用于生产StreamWriter类对象
    // 1、利用工厂类对象创建StreamWriter对象
    Json::StreamWriterBuilder swb;
    swb.settings_["emitUTF8"] = true;
    std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
    // 2、序列化
    std::stringstream ss;
    int ret = sw->write(value, &ss);
    if(ret != 0)
        return false;
    json_str = ss.str();
    return true;
}

bool unSerialize(const std::string& json_str, Json::Value& val)
{
    // Json::CharReader 反序列化类：用于将json格式字符串反序列化成Value类型
    // Json::CharReaderBuilder 工厂类：用于创建CharReader类对象
    // 1、利用工程类创建CharReader对象
    Json::CharReaderBuilder crb;
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
    // 2、反序列化
    std::string err;
    bool ret = cr->parse(json_str.c_str(), json_str.c_str() + json_str.size(), &val, &err);
    if(ret == false)
        return false;
    return true;
}

// 创建索引类
class ESIndex
{
public:
    ESIndex(std::shared_ptr<elasticlient::Client>& client, const std::string& index_name, const std::string& doc_type = "_doc"):
        _client(client), _index_name(index_name), _doc_type(doc_type)
        {
            // 构建请求正文框架
            Json::Value analysis, analyzer, ik, tokenizer;
            tokenizer["tokenizer"] = "ik_max_word";
            ik["ik"] = tokenizer;
            analyzer["analyzer"] = ik;
            analysis["analysis"] = analyzer;
            _index["settings"] = analysis;
        }
    
    // 给索引添加字段
    // key：字段名称，type：字段类型，analyzer：分词器类型，enabled：仅作存储还是搜索（false仅存储）
    ESIndex& append(const std::string& key, const std::string& type = "text", 
                const std::string& analyzer = "ik_max_word", bool enabled = true)
    {
        Json::Value fileds;
        fileds["type"] = type;
        fileds["analyzer"] = analyzer;
        fileds["enabled"] = enabled;
        _properties[key] = fileds;
        return *this; // 返回本体是为了方便调用函数时构成链式反应
    }

    // 发起构建索引请求
    bool create(const std::string& id = "default_index_id")
    {
        // 1、构建请求正文框架
        Json::Value mappings;
        mappings["dynamic"] = true;
        mappings["properties"] = _properties;
        _index["mappings"] = mappings;

        // 2、序列化
        std::string body;
        if(!serialize(_index, body))
        {
            LOG_ERROR("创建索引，序列化失败");
            return false;
        }
        
        // 3、发起客户端请求
        try
        {
            cpr::Response rsp =  _client->index(_index_name, _doc_type, id, body);
            if(rsp.status_code < 200 || rsp.status_code >= 300)
            {
                LOG_ERROR("创建索引{}失败，响应异常，异常状态码：{}", _index_name, rsp.status_code);
                return false;
            }
        }
        catch(std::exception& e)
        {
            LOG_ERROR("创建索引{}失败，请求异常：{}", _index_name, e.what());
            return false;
        }
        return true;
    }
private:
    std::string _index_name; // 索引名称
    std::string _doc_type; // 文档类型
    Json::Value _index; // 请求正文的Value类型
    Json::Value _properties; // 正文中的字段属性（有字段名称，字段类型，分词器类型等）
    std::shared_ptr<elasticlient::Client> _client; // 客户端
};

// 新增文档数据类
class ESInsert
{
public:
    ESInsert(std::shared_ptr<elasticlient::Client>& client, const std::string& index_name, const std::string& doc_type = "_doc"):
        _client(client), _index_name(index_name), _doc_type(doc_type)
        {}
    
    // 每次设置单个字段数据（设置一个文档中的单个字段）
    ESInsert& append(const std::string& key, const std::string& val) // 默认val是string类型先不考虑其他数据类型
    {
        _item[key] = val;
        return *this;
    }

    // 发起新增文档请求
    bool insert(const std::string& id = "") // 默认id为空，则服务器自动生成
    {
        // 1、序列化
        std::string body;
        if(!serialize(_item, body))
        {
            LOG_ERROR("新增文档，序列化失败");
            return false;
        }
        
        // 2、发起客户端请求
        try
        {
            cpr::Response rsp =  _client->index(_index_name, _doc_type, id, body);
            if(rsp.status_code < 200 || rsp.status_code >= 300)
            {
                LOG_ERROR("新增文档{}失败，响应异常，异常状态码：{}", body, rsp.status_code);
                return false;
            }
        }
        catch(std::exception& e)
        {
            LOG_ERROR("新增文档{}失败，请求异常：{}", body, e.what());
            return false;
        }
        return true;
    }
private:
    std::string _index_name; // 索引名称
    std::string _doc_type; // 文档类型
    Json::Value _item; // 一个文档数据
    std::shared_ptr<elasticlient::Client> _client; // 客户端
};

// 删除文档数据类
class ESRemove
{
public:
    ESRemove(std::shared_ptr<elasticlient::Client>& client, const std::string& index_name, const std::string& doc_type = "_doc"):
        _client(client), _index_name(index_name), _doc_type(doc_type)
        {
        }
    
    // 删除文档
    bool remove(const std::string& id) // 删除时文档id不能为空
    {
        try
        {
            cpr::Response rsp =  _client->remove(_index_name, _doc_type, id);
            if(rsp.status_code < 200 || rsp.status_code >= 300)
            {
                LOG_ERROR("删除id为{}的文档失败，响应异常，异常状态码：{}", id, rsp.status_code);
                return false;
            }
        }
        catch(std::exception& e)
        {
            LOG_ERROR("删除id为{}的文档失败，请求异常：{}", id, e.what());
            return false;
        }
        return true;
    }
private:
    std::string _index_name; // 索引名称
    std::string _doc_type; // 文档类型
    std::shared_ptr<elasticlient::Client> _client; // 客户端
};

// 搜索文档数据类
class ESSearch
{
public:
    ESSearch(std::shared_ptr<elasticlient::Client>& client, const std::string& index_name, const std::string& doc_type = "_doc"):
        _client(client), _index_name(index_name), _doc_type(doc_type)
        {
        }
    
    // 设置应该匹配的条件
    ESSearch& append_should(const std::string& key, const std::string& val)
    {
        Json::Value filed, match;
        filed[key] = val;
        match["match"] = filed;
        _should.append(match);
        return *this;
    }

    // 设置不匹配的条件
    ESSearch& append_must_not(const std::string& key, const std::vector<std::string>& vals)
    {
        Json::Value filed, terms;
        for(auto& val : vals)
            filed[key].append(val);
        terms["terms"] = filed;
        _must_not.append(terms);
        return *this;
    }

    // 发起搜索请求
    Json::Value search()
    {
        // 1、构建请求框架
        Json::Value cond;
        if(!_must_not.empty())  cond["must_not"] = _must_not;
        if(!_should.empty()) cond["should"] = _should;
        Json::Value bl, query;
        bl["bool"] = cond;
        query["query"] = bl;

        // 2、序列化
        std::string body;
        if(!serialize(query, body))
        {
            LOG_ERROR("检索文档，序列化失败");
            return Json::Value();
        }

        // 3、发起客户端请求
        cpr::Response rsp;
        try
        {
            rsp =  _client->search(_index_name, _doc_type, body);
            if(rsp.status_code < 200 || rsp.status_code >= 300)
            {
                LOG_ERROR("检索文档{}失败，响应异常，异常状态码：{}", body, rsp.status_code);
                return Json::Value();
            }
        }
        catch(std::exception& e)
        {
            LOG_ERROR("检索文档{}失败，请求异常：{}", body, e.what());
            return Json::Value();
        }

        // 4、对搜索结果进行发序列化
        Json::Value result;
        if(!unSerialize(rsp.text, result))
        {
            LOG_ERROR("检索文档，反序列化失败");
            return Json::Value();
        }
        return result["hits"]["hits"];
    }
private:
    std::string _index_name; // 索引名称
    std::string _doc_type; // 文档类型
    // Json::Value _must; // 一定要匹配的条件（无论_should满不满足，都要匹配）
    Json::Value _must_not; // 一定不匹配的条件
    Json::Value _should; // 应该匹配的条件（根据所给条件与文档字段数据进行匹配）
    std::shared_ptr<elasticlient::Client> _client; // 客户端
};